import { SpriteFrame, Prefab, _decorator, color, director, System, AudioClip, Material, isValid, Sprite, ImageAsset, SpriteAtlas, Texture2D, TextAsset } from 'cc';
import { BaseManager } from '../../scripts/libs/rxjs/cc3/BaseManager';
import { bundleLoad, bundleLoadDir, bundlePreload } from '../libs/rxjs/cc3/AssetManagerLoad';
import { concat, map, Observable, Observer } from 'rxjs';
import { ReactiveProperty } from '../libs/rxjs/cc3/ReactiveProperty';
import { assetManager } from 'cc';
import { ReactiveCommand } from '../libs/rxjs/cc3/ReactiveCommand';
import { nullifySubscription } from '../libs/rxjs/utils';
import { BindNodeDestroyed } from '../libs/rxjs/cc3/BindNode';
import { JsonAsset } from 'cc';
const { ccclass, property } = _decorator;

/**
 * 资源类型
 */
export enum ResType {
    Unknown = 'Unknown',
    Prefab = 'Prefab',
    PrefabDir = 'PrefabDir',
    SpriteFrame = 'SpriteFrame',
    SpriteAtlas = 'SpriteAtlas',
    Material = 'Material',
    AudioClip = 'AudioClip',
    AudioClipDir = 'AudioClipDir',
    TextAsset = 'TextAsset',
    TextAssetDir = 'TextAssetDir',
    JsonAsset = 'JsonAsset',
    JsonAssetDir = 'JsonAssetDir',
}

@ccclass('ResourceManager')
export class ResourceManager extends BaseManager {

    public static instance: ResourceManager;
    static ID = 'ResourceManager_system';
    get logColorOverride() { return color().fromHEX('#EECCDD') };

    static registerSystem() {
        director.unregisterSystem(director.getSystem(ResourceManager.ID));
        director.registerSystem(ResourceManager.ID, rm, System.Priority.MEDIUM);
        rm.init();
    }

    static unregisterSystem() {
        rm.destroy();
        director.unregisterSystem(director.getSystem(ResourceManager.ID));
    }

    onInitManager() {
        this.log('onInitManager', this.timestamp);
    }

    useObserves() {
        return [
        ];
    }

    preloadRes(resList: string[][], progress?: (finished: number, total: number) => void): Observable<any> {
        this.finishedCount.value = 0;
        let obs = [];
        for (let i = 0; i < resList.length; i++) {
            const [resPath, resType] = resList[i];
            let [bundleName, assetPath] = this.splitResPath(resPath);
            switch (resType) {
                case ResType.Prefab:
                    obs.push(bundlePreload(bundleName, assetPath, Prefab, (finished, total) => {
                        this.log('preloadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }));
                    break;
            }
        }
        return concat(...obs);
    }

    prefabs: Record<string, Prefab> = {};
    spriteFrames: Record<string, SpriteFrame> = {};
    spriteAtlas: Record<string, SpriteAtlas> = {};
    materials: Record<string, Material> = {};
    audioClips: Record<string, AudioClip> = {};
    textAssets: Record<string, TextAsset> = {};
    jsonAssets: Record<string, JsonAsset> = {};

    finishedCount = ReactiveProperty.Create(0);

    loadRes(resList: string[][], progress?: (finished: number, total: number) => void): Observable<any> {
        this.finishedCount.value = 0;
        let obs = [];
        for (let i = 0; i < resList.length; i++) {
            const [resPath, resType] = resList[i];
            let [bundleName, assetPath] = this.splitResPath(resPath);
            switch (resType) {
                case ResType.Prefab:
                    obs.push(bundleLoad(bundleName, assetPath, Prefab, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.prefabs[resPath] = _.data as Prefab;
                        this.prefabs[resPath].addRef();
                        return { status: 0, res: this.prefabs[resPath] };
                        //`loadRes ${resPath} (type:${resType}) success`;
                    })));
                    break;
                case ResType.PrefabDir:
                    obs.push(bundleLoadDir(bundleName, assetPath, Prefab, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        _.data.forEach((prefab: Prefab, idx) => {
                            let key = `${resPath}/${prefab.name}`;
                            this.prefabs[key] = prefab;
                            this.prefabs[key].addRef();
                        });
                        return { status: 0 };
                    })));
                    break;
                case ResType.SpriteFrame:
                    obs.push(bundleLoad(bundleName, assetPath, SpriteFrame, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.spriteFrames[resPath] = _.data as SpriteFrame;
                        this.spriteFrames[resPath].addRef();
                        return { status: 0, res: this.spriteFrames[resPath] };
                    })));
                    break;
                case ResType.SpriteAtlas:
                    obs.push(bundleLoad(bundleName, assetPath, SpriteAtlas, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.spriteAtlas[resPath] = _.data as SpriteAtlas;
                        this.spriteAtlas[resPath].addRef();
                        return { status: 0, res: this.spriteAtlas[resPath] };
                    })));
                    break;
                case ResType.Material:
                    obs.push(bundleLoad(bundleName, assetPath, Material, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.materials[resPath] = _.data as Material;
                        this.materials[resPath].addRef();
                        return { status: 0, res: this.materials[resPath] };
                    })));
                    break;
                case ResType.AudioClip:
                    obs.push(bundleLoad(bundleName, assetPath, AudioClip, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.audioClips[resPath] = _.data as AudioClip;
                        this.audioClips[resPath].addRef();
                        return { status: 0, res: this.audioClips[resPath] };
                    })));
                    break;
                case ResType.AudioClipDir:
                    obs.push(bundleLoadDir(bundleName, assetPath, AudioClip, (finished, total) => {
                        this.log('loadRes progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes failed:', resPath);
                            return { status: 1, resPath };
                        }
                        _.data.forEach((audioClip: AudioClip, idx) => {
                            let key = `${resPath}/${audioClip.name}`;
                            this.audioClips[key] = audioClip;
                            this.audioClips[key].addRef();
                        });
                        return { status: 0 };
                    })));
                    break;
                case ResType.TextAsset:
                    obs.push(bundleLoad(bundleName, assetPath, TextAsset, (finished, total) => {
                        this.log('loadRes(TextAsset) progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes(TextAsset) failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.textAssets[resPath] = _.data as TextAsset;
                        this.textAssets[resPath].addRef();
                        return { status: 0, res: this.textAssets[resPath] };
                    })));
                    break;
                case ResType.TextAssetDir:
                    obs.push(bundleLoadDir(bundleName, assetPath, TextAsset, (finished, total) => {
                        this.log('loadRes(TextAssetDir) progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes(TextAssetDir) failed:', resPath);
                            return { status: 1, resPath };
                        }
                        _.data.forEach((textAsset: TextAsset, idx) => {
                            let key = `${resPath}/${textAsset.name}`;
                            this.textAssets[key] = textAsset;
                            this.textAssets[key].addRef();
                        });
                        return { status: 0 };
                    })));
                case ResType.JsonAsset:
                    obs.push(bundleLoad(bundleName, assetPath, JsonAsset, (finished, total) => {
                        this.log('loadRes(JsonAsset) progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes(JsonAsset) failed:', resPath);
                            return { status: 1, resPath };
                        }
                        this.jsonAssets[resPath] = _.data as JsonAsset;
                        this.jsonAssets[resPath].addRef();
                        return { status: 0, res: this.jsonAssets[resPath] };
                    })));
                case ResType.JsonAssetDir:
                    obs.push(bundleLoadDir(bundleName, assetPath, JsonAsset, (finished, total) => {
                        this.log('loadRes(JsonAssetDir) progress:', resPath, finished, total);
                        if (progress != null) progress(finished, total);
                        this.finishedCount.value++;
                    }).pipe(map(_ => {
                        if (_?.data == null) {
                            this.warn('loadRes(JsonAssetDir) failed:', resPath);
                            return { status: 1, resPath };
                        }
                        _.data.forEach((jsonAsset: JsonAsset, idx) => {
                            let key = `${resPath}/${jsonAsset.name}`;
                            this.jsonAssets[key] = jsonAsset;
                            this.jsonAssets[key].addRef();
                        });
                        return { status: 0 };
                    })));
                    break;
            }
        }
        return concat(...obs);
    }

    releaseAudioClipCommand = ReactiveCommand.Create<string>();

    releaseRes(resList: string[][]) {
        for (let i = 0; i < resList.length; i++) {
            const [resPath, resType] = resList[i];
            let [bundleName, assetPath] = this.splitResPath(resPath);
            switch (resType) {
                case ResType.Prefab:
                    if (this.prefabs[resPath]) {
                        this.prefabs[resPath].decRef();
                        delete this.prefabs[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, Prefab);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.SpriteFrame:
                    if (this.spriteFrames[resPath]) {
                        this.spriteFrames[resPath].decRef();
                        delete this.spriteFrames[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, SpriteFrame);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.SpriteAtlas:
                    if (this.spriteAtlas[resPath]) {
                        this.spriteAtlas[resPath].decRef();
                        delete this.spriteAtlas[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, SpriteAtlas);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.Material:
                    if (this.materials[resPath]) {
                        this.materials[resPath].decRef();
                        delete this.materials[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, Material);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.AudioClip:
                    if (this.audioClips[resPath]) {
                        this.audioClips[resPath].decRef();
                        delete this.audioClips[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, SpriteAtlas);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.AudioClipDir:
                    for (let key in this.audioClips) {
                        if (key.startsWith(resPath)) {
                            assetManager.getBundle(bundleName).release(key, AudioClip);
                            this.audioClips[key].decRef();
                            delete this.audioClips[key];
                            this.log('releaseRes', key, resType);
                            this.releaseAudioClipCommand.execute(key);
                        }
                    }
                    break;
                case ResType.TextAsset:
                    if (this.textAssets[resPath]) {
                        this.textAssets[resPath].decRef();
                        delete this.textAssets[resPath];
                        assetManager.getBundle(bundleName).release(assetPath, TextAsset);
                        this.log('releaseRes', resPath, resType);
                    }
                    break;
                case ResType.TextAssetDir:
                    for (let key in this.textAssets) {
                        if (key.startsWith(resPath)) {
                            assetManager.getBundle(bundleName).release(key, TextAsset);
                            this.textAssets[key].decRef();
                            delete this.textAssets[key];
                            this.log('releaseRes', key, resType);
                        }
                    }
                    break;
            }
        }
    }

    splitResPath(resPath: string) {
        // this.log('@splitResPath', resPath);
        if (resPath == null) this.warn('@splitResPath resPath is null', resPath);
        let [firstPart, ...rest] = resPath.split("/");
        let secondPart = rest.join("/");
        return [firstPart, secondPart];
    }

    loadRemoteImageAsset(url: string): Observable<ImageAsset> {
        return new Observable(observer => {
            let _cancelled = false;
            assetManager.loadRemote<ImageAsset>(url, (err, imageAsset: ImageAsset) => {
                if (_cancelled) return;
                observer.next(imageAsset);
                observer.complete();
            });
            return () => {
                _cancelled = true;
            }
        });
    }

    // 加载远程图片到sprite
    loadSpriteFrameWithRemoteUrl(sprite: Sprite, url: string) {
        if (!isValid(sprite?.node)) return;
        this.loadRemoteImageAsset(url).subscribe(imageAsset => {
            if (imageAsset == null) {
                sprite.spriteFrame = null;
            } else {
                if (!isValid(sprite?.node)) return;
                let spriteFrame = new SpriteFrame();
                const texture = new Texture2D();
                texture.image = imageAsset;
                spriteFrame.texture = texture;
                imageAsset.addRef();
                sprite.spriteFrame = spriteFrame;
                spriteFrame.addRef();

                nullifySubscription.call(sprite.node, '_sub_destroy');
                let _sub_destroy = BindNodeDestroyed(sprite.node).subscribe(_ => {
                    this.releaseSpriteFrameWithRemoteUrl(sprite);
                });
                sprite.node.attr({ _sub_destroy });
            }
        });
    }

    // 释放sprite上的远程图片
    releaseSpriteFrameWithRemoteUrl(sprite: Sprite) {
        if (sprite && sprite.spriteFrame) {
            const spriteFrame = sprite.spriteFrame;
            sprite.spriteFrame.decRef(false);
            sprite.spriteFrame = null;
            if (spriteFrame.refCount <= 0) {
                let texture = spriteFrame.texture as Texture2D;
                if (texture) {
                    texture.image?.decRef(true);
                    texture.destroy();
                } else {
                    this.warn('try release Texture, but texture is null:', spriteFrame);
                }
                spriteFrame.destroy();
            }
        }
    }

}
globalThis.ResourceManager = ResourceManager;

export const rm = ResourceManager.instance = new ResourceManager();